package org.ribizli.basicgameserver;

import java.io.IOException;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import org.ribizli.basicgameserver.Game;

public class BasicGameServer extends HttpServlet {

  private static int TIMEOUT = 30000;
  private static Logger logger = LogManager.getLogger(BasicGameServer.class);
  
  private static final long serialVersionUID = -5549422480702296289L;
  private Hashtable<String, Game> gameMap;
  private Hashtable<String, Game> tokenToGame;

	private Hashtable<String, TimeOutTimerTask> tokenLastActive;
  private Timer timeOutTimer;

  public static final String JSON_CONTENT_TYPE = "application/json";

  // REQUEST
  public static final String FIELD_TASK = "task";
  public static final String FIELD_USERNAME = "username";
  public static final String FIELD_PASSWORD = "password";
  public static final String FIELD_GAMEID = "game_id";
  public static final String FIELD_MESSAGES = "messages";
  public static final String FIELD_DETAILS = "details";

  // RESPONSE
  public static final String FIELD_ERROR = "error";
  public static final String FIELD_TOKEN = "token";
  public static final String FIELD_STATUS = "status";

  public static final String TASK_LOGIN = "login";
  public static final String TASK_LOGOUT = "logout";
  public static final String TASK_MESSAGE = "message";
  public static final String TASK_REGISTER = "register";
  public static final String TASK_TOPLIST = "toplist";

  public static final String ERROR_GAME_NOT_FOUND = "GAME_NOT_FOUND";
  public static final String ERROR_INVALID_MESSAGE = "INVALID_MESSAGE";
  public static final String ERROR_INVALID_TOKEN = "INVALID_TOKEN";

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    this.doPost(req, resp);
  }

	@Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
	  // TODO: this is for reseting the server
    if (req.getParameter("reset") != null) {
      reset();
      resp.getOutputStream().println("OK");
      return;
    }
    
    resp.setContentType(JSON_CONTENT_TYPE);
    String task = req.getParameter(FIELD_TASK);
    if (TASK_LOGIN.equals(task)) {
      String username = req.getParameter(FIELD_USERNAME);
      String password = req.getParameter(FIELD_PASSWORD);
      String gameId = req.getParameter(FIELD_GAMEID);
      if (username == null || password == null || gameId == null) {
      	logger.warn("'game_id', 'username' or 'password' not sent for login");
      	writeError(resp, ERROR_INVALID_MESSAGE);
      	return;
      }
      logger.debug("Login request - username: " + username + ", game: "
          + gameId);
      Game game = gameMap.get(gameId);
      if (game == null) {
      	logger.warn("No such game: " + gameId);
        writeError(resp, ERROR_GAME_NOT_FOUND);
        return;
      }
      try {
      	JSONObject info = new JSONObject();
      	String token = null;
      	synchronized (BasicGameServer.this) {
	        token = game.newPlayer(username, password, info);
	        tokenToGame.put(token, game);
	        TimeOutTimerTask tt = null;
	        tt = tokenLastActive.remove(token);
	        if (tt != null) {	tt.cancel(); }
	        tt = new TimeOutTimerTask(token);
	        tokenLastActive.put(token, tt);
	        timeOutTimer.schedule(tt, TIMEOUT);
      	}
        logger.info("token generated: " + token);
        try {
          writeResponse(resp, new JSONObject().put(FIELD_TOKEN, token).put(
							FIELD_DETAILS, info));
        } catch (JSONException e) {
          // key is never null
        }
      } catch (GameException e) {
      	logger.log(Level.WARN, e.getMessage(), e);
        writeError(resp, e.getMessage());
        return;
      }
    } else if (TASK_LOGOUT.equals(task)) {
      String token = req.getParameter(FIELD_TOKEN);
      if (token == null) {
      	logger.warn("token not provided for logout");
        writeError(resp, ERROR_INVALID_MESSAGE);
        return;
      }
      logger.info("Logout request - token: " + token);
      Game game = tokenToGame.get(token);
      if (game == null) {
      	logger.warn("token not registered, cannot log out");
        writeError(resp, ERROR_INVALID_TOKEN);
        return;
      }
    	synchronized (BasicGameServer.this) {
	      game.playerLeft(token);
	      tokenToGame.remove(token);
	      tokenLastActive.remove(token).cancel();
    	}
      writeResponse(resp, getStatusJSON("logout"));
    } else if (TASK_MESSAGE.equals(task)) {
      String token = req.getParameter(FIELD_TOKEN);
      if (token == null) {
      	logger.warn("token not provided for messages");
        writeError(resp, ERROR_INVALID_MESSAGE);
        return;
      }
      Game game = tokenToGame.get(token);
      if (game == null) {
      	logger.warn("token not registered, cannot send message");
        writeError(resp, ERROR_INVALID_TOKEN);
        return;
      }
      String rawMessages = req.getParameter(FIELD_MESSAGES);
      if (rawMessages == null) {
      	logger.warn("empty message was sent");
        writeError(resp, ERROR_INVALID_MESSAGE);
        return;
      }
    	synchronized (BasicGameServer.this) {
	      tokenLastActive.remove(token).cancel();
	      TimeOutTimerTask tt = new TimeOutTimerTask(token);
	      tokenLastActive.put(token, tt);
	      timeOutTimer.schedule(tt, TIMEOUT);
    	}
      try {
        JSONObject response = game.messages(token, new JSONObject(rawMessages));
        writeResponse(resp, response);
      } catch (GameException e) {
      	logger.log(Level.WARN, e.getMessage(), e);
        writeError(resp, e.getMessage());
      } catch (JSONException e) {
      	logger.log(Level.WARN, e.getMessage(), e);
        writeError(resp, ERROR_INVALID_MESSAGE);
      }
    } else if (TASK_REGISTER.equals(task)) {
      String username = req.getParameter(FIELD_USERNAME);
      String password = req.getParameter(FIELD_PASSWORD);
      String details = req.getParameter(FIELD_DETAILS);
      String gameId = req.getParameter(FIELD_GAMEID);
      if (username == null || password == null || details == null || gameId == null) {
      	logger.warn("'game_id', 'username', 'password' or 'details' not set for registration");
      	writeError(resp, ERROR_INVALID_MESSAGE);
      	return;
      }
      logger.info("Registration request - username: " + username + ", game: "
          + gameId);
      Game game = gameMap.get(gameId);
      if (game == null) {
      	logger.warn("No such game (registration): " + gameId);
        writeError(resp, ERROR_GAME_NOT_FOUND);
        return;
      }
      try {
				game.register(username, password, getMapFromJSON(details));
				writeResponse(resp, getStatusJSON("register"));
			} catch (JSONException e) {
				logger.log(Level.WARN, details, e);
				writeError(resp, ERROR_INVALID_MESSAGE);
			} catch (GameException e) {
				logger.log(Level.WARN, e.getMessage(), e);
				writeError(resp, e.getMessage());
			}
    } else if (TASK_TOPLIST.equals(task)) {
      String username = req.getParameter(FIELD_USERNAME);
      String gameId = req.getParameter(FIELD_GAMEID);
      if (gameId == null) {
      	logger.warn("'game_id' not sent for toplist");
      	writeError(resp, ERROR_INVALID_MESSAGE);
      	return;
      }
      logger.debug("Toplist request - username: " + username + ", game: "
          + gameId);
      Game game = gameMap.get(gameId);
      if (game == null) {
      	logger.warn("No such game: " + gameId);
        writeError(resp, ERROR_GAME_NOT_FOUND);
        return;
      }
      try {
				writeResponse(resp, game.getTopScores(username));
			} catch (GameException e) {
      	logger.log(Level.WARN, e.getMessage(), e);
        writeError(resp, e.getMessage());
			}
      
    } else {
      writeError(resp, ERROR_INVALID_MESSAGE);
    }
  }

	@SuppressWarnings("unchecked")
	public static Map<String, String> getMapFromJSON(String json)
			throws JSONException {
		JSONObject detailsO;
		detailsO = new JSONObject(json);
		HashMap<String, String> m = new HashMap<String, String>();
		for (Iterator<String> iterator = detailsO.keys(); iterator.hasNext();) {
			String key = iterator.next();
			m.put(key, detailsO.getString(key));
		}
		return m;
	}

  private JSONObject getStatusJSON(String task) {
  	JSONObject obj = null;
  	try {
			obj = new JSONObject("{"+FIELD_STATUS+":'OK',"+FIELD_TASK+"='"+task+"'}");
		} catch (JSONException e) {
		}
		return obj;
	}

	private void writeResponse(HttpServletResponse resp, JSONObject json)
      throws IOException {
    resp.getWriter().println(json.toString());
  }

  private void writeError(HttpServletResponse resp, String message)
      throws IOException {
    JSONObject json = new JSONObject();
    try {
      json.put(FIELD_ERROR, message);
    } catch (JSONException e) {
    }
    writeResponse(resp, json);
    logger.warn(message);
  }

  @SuppressWarnings("unchecked")
  @Override
  public void init() throws ServletException {
    super.init();
    
    ServletConfig conf = getServletConfig();
    String gamesString = conf.getInitParameter("games");
    if (gamesString == null) {
    	throw new ServletException("'gamesString' initparam not set");
    }
    String[] games = gamesString.split("\\s*,\\s*");
    gameMap = new Hashtable<String, Game>();
    for (int i = 0; i < games.length; i++) {
    	try {
				Class<Game> cl = (Class<Game>) Class.forName(games[i]);
				Game game = cl.newInstance();
				game.init();
				gameMap.put(game.getID(), game);
			} catch (Exception e) {
				logger.log(Level.ERROR, "Error init " + games[i], e);
			}
		}
    tokenToGame = new Hashtable<String, Game>();
    tokenLastActive = new Hashtable<String, TimeOutTimerTask>();
    timeOutTimer = new Timer();
    String timeout = conf.getInitParameter("timeout");
    if (timeout != null) {
    	TIMEOUT = Integer.parseInt(timeout) * 1000;
    	logger.info("TIMEOUT: " + TIMEOUT);
    }
  }
  
  @Override
	public void destroy() {
		super.destroy();
		timeOutTimer.cancel();
		logger.info("Timer cancelled");
	}
  
  @SuppressWarnings("unchecked")
  private void reset() {
    for (String gameId : gameMap.keySet()) {
      try {
        Class<Game> cl = (Class<Game>) gameMap.get(gameId).getClass();
        Game game = cl.newInstance();
        game.init();
        gameMap.put(game.getID(), game);
      } catch (Exception e) {
        logger.log(Level.ERROR, "Error init " + gameId, e);
      }
    }
    tokenToGame = new Hashtable<String, Game>();
    tokenLastActive = new Hashtable<String, TimeOutTimerTask>();
    timeOutTimer = new Timer();
  }

  private class TimeOutTimerTask extends TimerTask {
  	
  	private String token;
    
		private TimeOutTimerTask(String token) {
      this.token = token;
    }

    @Override
    public void run() {
    	synchronized (BasicGameServer.this) {
    		TimeOutTimerTask t = tokenLastActive.get(this.token);
    		if (!this.equals(t)) {
    			// Already a new TT is scheduled (or no one)
    			return; 
    		}
    		tokenLastActive.remove(this.token);
	      tokenToGame.remove(this.token).playerLeft(this.token);
    	}
      logger.info("User timeout: " + this.token);
    }
    
    public String getToken() {
    	return token;
    }
    
  }
}
